using System;
using System.Collections.Generic;
using System.IO;

using Pegasus.Diagnostics;
using Pegasus.Log4Net;
using Pegasus.Runtime.Serialization.Formatters.Xml;

namespace Pegasus.Workflow.Service.FileServices
{
	/// <summary>
	/// Uses the file system to persist the workflow context objects
	/// </summary>
	public class FilePersistenceService : WorkflowPersistenceService, IDisposable
	{
		// Local Instance Values
		private ILog m_log = LogManager.GetLogger( typeof( FilePersistenceService ) );

		private object m_lock = new object();

		private string m_directory;
		private Dictionary<int, WorkflowContext> m_contextTable = new Dictionary<int, WorkflowContext>();

		// Local Const Values 
		private const string ContextFileLocked = "_FilePersistenceService.FileLocked";

		/// <summary>
		/// Initializes a new instance of the <see cref="T:FilePersistenceService"/> class.
		/// </summary>
		/// <param name="directory">The directory.</param>
		public FilePersistenceService( string directory )
		{
			m_log.DebugFormat( "FilePersistenceService( directory = {0} )", directory );

			// Check Parameters
			ParamCode.AssertNotEmpty( directory, "directory" );
			ParamCode.Assert( Directory.Exists( directory ), "directory", "The directory {0} does not exist or is inaccessable", directory );

			m_directory = directory;
		}

		/// <summary>
		/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			m_log.Debug( "FilePersistenceService:Dispose()" );
			m_contextTable.Clear();
		}

		/// <summary>
		/// Starts this instance of the service.
		/// </summary>
		/// <param name="workflowService"></param>
		public override void Start( WorkflowService workflowService )
		{
			m_log.DebugFormat( "FilePersistenceService:Start( workflowService = {0} )", workflowService );

			lock( m_lock )
			{
				base.Start( workflowService );
				m_contextTable.Clear();
			}
		}

		/// <summary>
		/// Stops this instance of the service.
		/// </summary>
		public override void Stop()
		{
			m_log.DebugFormat( "FilePersistenceService:Stop()" );

			lock( m_lock )
			{
				base.Stop();
				Dispose();
			}
		}

		/// <summary>
		/// Registers a new workflow context with the persistence service.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <returns></returns>
		protected override int OnRegisterNewWorkflowContext( WorkflowContext context )
		{
			m_log.DebugFormat( "FilePersistenceService:OnRegisterNewWorkflowContext( context = {0} )", context );

			// Check Parameters
			ParamCode.AssertNotNull( context, "context" );

			lock( m_lock )
			{
				// Find a new workflowId/filename for the workflow
				int workflowId = GetNextWorkflowId();

				// Open/Create the file so it exist on the disk
				using( FileStream fileStream = OpenWorkflowFile( workflowId, FileMode.Create ) )
				{
					context[ ContextFileLocked ] = true;
				}

				// Set the new workflow context
				InitializeNewContext( context, workflowId );
				m_contextTable[ workflowId ] = context;

				return workflowId;
			}
		}


		/// <summary>
		/// Saves the workflow context.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <param name="unlock">if set to <c>true</c> [unlock].</param>
		protected override void OnSaveWorkflowContext( WorkflowContext context, bool unlock )
		{
			m_log.DebugFormat( "FilePersistenceService:OnSaveWorkflowContext( context = {0}, unlock = {1} )", context, unlock );

			// Check Parameters
			ParamCode.AssertNotNull( context, "context" );

			lock( m_lock )
			{
				// Is the context locked so that we can write the file out
				bool fileLocked = false;
				if( context.TryGetValue<bool>( ContextFileLocked, out fileLocked ) )
				{
					if( !fileLocked )
					{
						throw new WorkflowLockedException( context.WorkflowId, "Can not save the workflow because the context is not locked." );
					}

					if( context.IsReadOnly )
					{
						throw new WorkflowLockedException( context.WorkflowId, "Can not save the workflow because the context is read-only." );
					}

					// Open the file and write out the data
					try
					{
						using( FileStream fileStream = OpenWorkflowFile( context.WorkflowId, FileMode.Create ) )
						{
							// Write out the context object
							new XmlFormatter2().Serialize( fileStream, context );
						}
					}
					catch( Exception e )
					{
						m_log.Error( "FilePersistenceService:OnSaveWorkflowContext: Exception", e );
						throw;
					}
					finally
					{
						if( unlock )
						{
							OnReleaseWorkflowContext( context );
						}
					}
				}
				else
				{
					throw new WorkflowLockedException( context.WorkflowId, "Can not save the workflow because the context is not locked (missing lock value)." );
				}
			}
		}

		/// <summary>
		/// Loads the workflow context.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		/// <param name="readOnly">if set to <c>true</c> [read only].</param>
		/// <returns></returns>
		protected override WorkflowContext OnLoadWorkflowContext( int workflowId, bool readOnly )
		{
			m_log.DebugFormat( "FilePersistenceService:OnLoadWorkflowContext( workflowId = {0}, readOnly = {1} )", workflowId, readOnly );

			// Check Parameters
			ParamCode.AssertRange( workflowId, 1, int.MaxValue, "workflowId" );

			lock( m_lock )
			{
				if( readOnly )
				{
					return LoadReadOnlyWorkflowContext( workflowId );
				}

				return LockAndLoadWorkflowContext( workflowId );
			}
		}

		/// <summary>
		/// Releases the workflow context.
		/// </summary>
		/// <param name="context">The context.</param>
		protected override void OnReleaseWorkflowContext( WorkflowContext context )
		{
			m_log.DebugFormat( "FilePersistenceService:OnReleaseWorkflowContext( context = {0} )", context );

			// Check Parameter
			ParamCode.AssertNotNull( context, "context" );

			lock( m_lock )
			{
				// Set the lock value to false
				context[ ContextFileLocked ] = false;

				// Release the context and remove it from our table.
				SetContextAsReadOnly( context );
				m_contextTable.Remove( context.WorkflowId );
			}
		}

		/// <summary>
		/// Locks the and load workflow context.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		/// <returns></returns>
		private WorkflowContext LockAndLoadWorkflowContext( int workflowId )
		{
			m_log.DebugFormat( "FilePersistenceService:LockAndLoadWorkflowContext( workflowId = {0} )", workflowId );

			// Check Parameters
			ParamCode.AssertRange( workflowId, 1, int.MaxValue, "workflowId" );

			// If the file already loaded return it
			if( m_contextTable.ContainsKey( workflowId ) )
			{
				return m_contextTable[ workflowId ];
			}

			WorkflowContext context = null;
			using( FileStream fileStream = OpenWorkflowFile( workflowId, FileMode.Open ) )
			{
				context = ReadInWorkflowContext( fileStream );
				context[ ContextFileLocked ] = true;
			}

			InitializeExistingContext( workflowId, context, false );
			m_contextTable[ workflowId ] = context;

			return context;
		}

		/// <summary>
		/// Loads the read only workflow context.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		/// <returns></returns>
		private WorkflowContext LoadReadOnlyWorkflowContext( int workflowId )
		{
			m_log.DebugFormat( "FilePersistenceService:LoadReadOnlyWorkflowContext( workflowId = {0} )", workflowId );

			// Check Parameters
			ParamCode.AssertRange( workflowId, 1, int.MaxValue, "workflowId" );

			using( FileStream fileStream = OpenWorkflowFile( workflowId, FileMode.Open ) )
			{
				WorkflowContext context = ReadInWorkflowContext( fileStream );
				InitializeExistingContext( workflowId, context, true );
				return context;
			}
		}

		/// <summary>
		/// Opens the filename.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		/// <param name="mode">The open mode for the file.</param>
		/// <returns></returns>
		private FileStream OpenWorkflowFile( int workflowId, FileMode mode )
		{
			m_log.DebugFormat( "FilePersistenceService:OpenWorkflowFile( workflowId = {0}, mode = {1} )", workflowId, mode );

			// Check Parameters
			ParamCode.AssertRange( workflowId, 1, int.MaxValue, "workflowId" );

			FileStream fileStream = null;
			try
			{
				// Open the file
				fileStream = File.Open( GetFilenameFromWorkflowId( workflowId ), mode, FileAccess.ReadWrite );
			}
			catch( FileNotFoundException e )
			{
				m_log.Error( "FilePersistenceService:OpenWorkflowFile: File not found exception.", e );
				throw new WorkflowNotFoundException( workflowId, e );
			}
			catch( Exception e )
			{
				m_log.Error( "FilePersistenceService:OpenWorkflowFile: Exception.", e );
				throw new WorkflowException( e, "Unable to open workflow {0}", workflowId );
			}

			return fileStream;
		}

		/// <summary>
		/// Reads the in workflow context.
		/// </summary>
		/// <param name="fileStream">The file stream.</param>
		/// <returns></returns>
		private WorkflowContext ReadInWorkflowContext( FileStream fileStream )
		{
			m_log.DebugFormat( "FilePersistenceService:ReadInWorkflowContext( fileStream = {0} )", fileStream );

			try
			{
				// Read in the context object.
				return (WorkflowContext) new XmlFormatter2().Deserialize( fileStream );
			}
			catch( Exception e )
			{
				m_log.Error( "FilePersistenceService:ReadInWorkflowContext: Exception.", e );
				
				// TODO: Clean up.  This causes an error in the VS2008 Beta 2 compiler so I've 
				// gotten it to work with the following line.  Whe the VS ships undo this and
				// see if its still a problem.
				//throw new WorkflowException( e, "Unable to deserialize the workflow context" );
				throw new WorkflowException( e, "Unable to deserialize the workflow context {0}", "" );
			}
		}

		/// <summary>
		/// Gets the filename from workflow id.
		/// </summary>
		/// <param name="workflowId">The workflow id.</param>
		/// <returns></returns>
		private string GetFilenameFromWorkflowId( int workflowId )
		{
			m_log.DebugFormat( "FilePersistenceService:GetFilenameFromWorkflowId( workflowId = {0} )", workflowId );

			// Check Parameters
			ParamCode.AssertRange( workflowId, 1, int.MaxValue, "workflowId" );

			return Path.Combine( m_directory, string.Format( "Workflow_{0}.xml", workflowId ) );
		}

		/// <summary>
		/// Gets the workflow next workflow id.
		/// </summary>
		/// <returns></returns>
		private int GetNextWorkflowId()
		{
			m_log.Debug( "FilePersistenceService:GetNextWorkflowId()" );

			int workflowId = 1;

			string infoFilename = Path.Combine( m_directory, "FilePersistenceServiceInfo.xml" );

			// If the files does not exist then create the default
			if( File.Exists( infoFilename ) )
			{
				using( FileStream input = File.Open( infoFilename, FileMode.Open, FileAccess.Read, FileShare.None ) )
				{
					workflowId = (int) new XmlFormatter2().Deserialize( input );
				}

				workflowId++;
			}

			// Find a new workflowId/filename for the workflow
			string filename = GetFilenameFromWorkflowId( workflowId );
			while( File.Exists( filename ) )
			{
				workflowId++;
				filename = GetFilenameFromWorkflowId( workflowId );
			}

			// Update/Create the file.
			using( FileStream output = File.Open( infoFilename, FileMode.Create, FileAccess.Write, FileShare.None ) )
			{
				new XmlFormatter2().Serialize( output, workflowId );
			}

			return workflowId;
		}
	}
}
